Teil 8 - Einleitung für Pläne

Kontext

Hier wird ein entscheidendes Objekt vorgestellt, um Federated Learning in industriellen Größenordnungen umzusetzen: der Plan
Er reduziert die benötigte Bandbreite dramatisch, ermöglich asynchrone Ansätze und gewährt den ferngesteuerten Helfern mehr Autonomie. Das original Konzept für Pläne kann in dem Paper "Towards Federated Learning at Scale: System Design" nachgelsesen werden. In diesem Fall wurde es auf die Anforderungen der PySyft Bibiliothek angepasst.

Ein Plan ist zum Speichern einer Aneinanderreihung von Torch Operationen gedacht. Damit gleicht er einer Funktion, kann jedoch diese Aneinanderreihung zu ferngesteuerten Helfern senden und dabei eine Referenz darauf selbst behalten. Somit kann eine Sequenz von $n$ Operationen, mit den zugehörigen Pointern, mittels einer einzigen Nachricht übermittelt werden, anstatt für jede der Operationen eine eigene Nachricht senden zu müssen. Es ist sogar möglich festgelegte Tensoren (sogenannte State Tensoren) mitzusenden und damit erweiterte Funktionalitäten zu nutzen. Pläne können entweder als zu sendende Funktion oder als send- und ausführbare Klasse aufgefasst werden.
Infolgedessen können Nutzer die Pläne auf höchstem Level als magische Fähigkeit auffassen, welche es erlaubt Sequenzen von Torch Funktionen auf Geräten hintereinander ausführen zu lassen.

Eine wichtige Anmerkung ist, dass die Klasse aller verwendbaren Funktionen in den Plänen momentan noch ausschließlich auf Aneinanderreihungen von Torch Operationen limitiert sind. Dies schließt speziell die logischen Strukturen wie if, for und while aus, auch wenn aktuell an Notlösungen gearbeitet wird. Um es ganz genau zu nehmen, können die logischen Statements doch eingebaut werden, allerdings legt die erste Auswertung des Statements fest, wie es im Folgenden jedesmal ausgeführt wird. In den meisten Fällen ist solches Verhalten nicht wünschenswert.

Autoren:

Übersetzer:

Importe und Model Spezifikationen

Zuerst werden die offiziellen Importe getätigt.


In [ ]:
import torch
import torch.nn as nn
import torch.nn.functional as F

Danach folgen die Importe spezifisch zu PySyft. Eine wichtige Anmerkung ist hier, dass ein lokaler Helfer kein Klient-Helfer sein sollte. Nur nicht-Klienten-Helfer können Objekte speichern, was für die Fähigkeit zum Ausführen eines Planes bedeutsam ist.


In [ ]:
import syft as sy  # import the Pysyft library
hook = sy.TorchHook(torch)  # hook PyTorch ie add extra functionalities 

# IMPORTANT: Local worker should not be a client worker
hook.local_worker.is_client_worker = False


server = hook.local_worker

Die ferngesteuerten Helfer oder Geräte werden nach dem Schema aus dem referenzierten Artikel benannt.
Anschließend werden die Helfer mit Daten ausgestattet.


In [ ]:
x11 = torch.tensor([-1, 2.]).tag('input_data')
x12 = torch.tensor([1, -2.]).tag('input_data2')
x21 = torch.tensor([-1, 2.]).tag('input_data')
x22 = torch.tensor([1, -2.]).tag('input_data2')

device_1 = sy.VirtualWorker(hook, id="device_1", data=(x11, x12)) 
device_2 = sy.VirtualWorker(hook, id="device_2", data=(x21, x22))
devices = device_1, device_2

Basis Beispiel

Eine Funktion wird definiert, welche anschließend in einen Plan umgewandelt werden soll. Um dies zu erreichen, reicht es aus einen passenden Dekorator über die Funktion zu schreiben!


In [ ]:
@sy.func2plan()
def plan_double_abs(x):
    x = x + x
    x = torch.abs(x)
    return x

Eine Überprüfung bestätigt den Erfolg.


In [ ]:
plan_double_abs

Um einen Plan zu verwenden, müssen zwei Dinge abgeschlossen sein:

  • das Bauen des Plans (das Registrieren der Sequenz an Operationen in der Funktion)
  • das Senden des Plans zum Helfer / Gerät

Bauen des Plans

Um den Plan zu bauen, muss er nur mit einigen Daten aufgerufen werden.

Gestartet wird mit dem Aufruf einiger Daten:
Eine Anfrage wird dabei über das Netzwerk gesendet und ein Referenz Pointer auf die Daten zurückgegeben.


In [ ]:
pointer_to_data = device_1.search('input_data')[0]
pointer_to_data

Beim Versuch den Plan auf den Daten des Gerätes location:device_1 auszuführen, wird ein Fehler verursacht werden, da der Plan noch nicht gebaut wurde.


In [ ]:
plan_double_abs.is_built

In [ ]:
# Sending non-built Plan will fail
try:
    plan_double_abs.send(device_1)
except RuntimeError as error:
    print(error)

Um den Plan zu bauen, muss er nur mit den benötigten Argumenten (a. k. a. einigen Daten) und der build Methode aufgerufen werden. Nachdem ein Plan gebaut wurde, wird er alle aneinander gereihten Befehle ausführen und in seinem Attribut actions abspeichern!


In [ ]:
plan_double_abs.build(torch.tensor([1., -2.]))

In [ ]:
plan_double_abs.is_built

Wird der Plan nun gesendet, so funktionert es!


In [ ]:
# This cell is executed successfully
pointer_plan = plan_double_abs.send(device_1)
pointer_plan

Genau wie bei Tensoren, wird ein Pointer auf das gesendete Objekt zurückgegeben. In diesem Falle ist es ein PointerPlan.

Es ist wichtig sich in Erinnerung zu rufen, dass beim Bauen des Plans alle Ids der Speicherorte für die Ergebnisse festgelegt werden, bevor die eigentlichen Berechnungen starten. Dies ermöglicht ein asynchrones senden der Befehle, da ein Referenz Pointer zurückgegeben wird, bevor die Berechnungen auf dem Helfer abgeschlossen sind. Somit lässt sich mit lokalen Befehlen fortfahren, ohne auf den Helfer warten zu müssen. Eine wichtige Anwendung dafür ist z. B. das Starten einer Berechnung eines Daten-Batches auf Gerät_1 und ohne auf das Ergebnis abwarten zu müssen, kann auf Gerät_2 ein weiterer Daten-Batch bearbeitet werden.

Starten eines Planes aus der Ferne

Der Plan kann nun aus der Ferne gestartet werden, indem der Pointer zum Plan mit einem Pointer zu den Daten aufgerufen wird. Dies veranlasst die Ausführung des Plans und die Ergebnisse werden an den vorher festgelegten Orten abgelegt. Ein Pluspunkt ist, dass alles nur einer einzigen Kommunikationsrunde bedurfte.

Das Ergebnis ist ein einfacher Pointer, wie er schon von den normalen Torch Funktionen bekannt ist!


In [ ]:
pointer_to_result = pointer_plan(pointer_to_data)
print(pointer_to_result)

Dieses Ergebnis kann einfach zurückgeholt werden.


In [ ]:
pointer_to_result.get()

Einem konkreten Beispiel entgegen

Eigentlich soll solch ein Plan jedoch für Deep und Federated Learning genutzt werden, nicht wahr? Lassen Sie uns ein komplizierteres Beispiel mit einem Neuronalen Netzwerk betrachten.
Anzumerken ist, dass nun eine Klasse in einen Plan übertragen wird. Dies kann erreicht werden, indem die neue Klasse von der sy.Plan Klasse erbt (anstelle des üblichen nn.Module).


In [ ]:
class Net(sy.Plan):
    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(2, 3)
        self.fc2 = nn.Linear(3, 2)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return F.log_softmax(x, dim=0)

In [ ]:
net = Net()

In [ ]:
net

Der Plan wird mit einigen Pseudo-Daten gebaut.


In [ ]:
net.build(torch.tensor([1., 2.]))

Nun wird der Plan an den ferngesteuerten Helfer gesendet.


In [ ]:
pointer_to_net = net.send(device_1)
pointer_to_net

Anschließend werden noch einige Daten benötigt.


In [ ]:
pointer_to_data = device_1.search('input_data')[0]

Die Syntax ist nun identisch zum normalen Ausführen von Befehlen auf der lokalen Maschine. Verglichen mit der klassischen Fernsteuerung wird jedoch nur eine einzige Kommunikationsrunde für die Ausführung benötigt.


In [ ]:
pointer_to_result = pointer_to_net(pointer_to_data)
pointer_to_result

Das Ergebnis lässt sich wie gewöhnlich erhalten!


In [ ]:
pointer_to_result.get()

Et voilà! Die Kommunikation zwischen lokaler Maschine (oder dem Server) und dem ferngesteuerten Gerät konnte dramatisch reduziert werden.

Wechseln zwischen Helfern

Eine weitere wichtige und wünschenswerte Fähigkeit ist das Wiederverwenden des Planes auf mehreren Helfern mit unterschiedlichen Daten-Batches.
Ein Neubau des Planes beim Wechsel des Helfers soll hierbei vermieden werden. Im Folgenden wird das obrige Beispiel daraufhin angepasst.


In [ ]:
class Net(sy.Plan):
    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(2, 3)
        self.fc2 = nn.Linear(3, 2)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return F.log_softmax(x, dim=0)

In [ ]:
net = Net()

# Build plan
net.build(torch.tensor([1., 2.]))

Dies sind die wichtigsten Schritte die auszuführen waren.


In [ ]:
pointer_to_net_1 = net.send(device_1)
pointer_to_data = device_1.search('input_data')[0]
pointer_to_result = pointer_to_net_1(pointer_to_data)
pointer_to_result.get()

Tatsächlich lassen sich andere PointerPlans einfach vom selben Plan aus nutzen und so bleibt die Syntax fürs Verwenden auf anderen Geräten identisch.


In [ ]:
pointer_to_net_2 = net.send(device_2)
pointer_to_data = device_2.search('input_data')[0]
pointer_to_result = pointer_to_net_2(pointer_to_data)
pointer_to_result.get()

Anmerkung: Aktuell lässt sich mit der Plan Klasse nur eine einzige Methode namens "forward" verwenden.

Automatisch Pläne bauen, welche auch Funktionen sind

Für Funktionen (@ sy.func2plan) kann der Plan automatisch gebaut werden ohne explizit die Methode build aufrufen zu müssen. In solchen Fällen ist der Plan direkt beim Erstellen gebaut.

Um diese Funktionalität direkt nutzen zu können, muss der Dekorator nur mit dem zusätzlichen Argument args_shape aufgerufen werden. Dieses muss eine Liste aller Shapes der Funktions-Argumente enthalten.


In [ ]:
@sy.func2plan(args_shape=[(-1, 1)])
def plan_double_abs(x):
    x = x + x
    x = torch.abs(x)
    return x

plan_double_abs.is_built

Der args_shape Parameter wird intern genutzt um Pseudo-Tensoren zu erschaffen, welche wiederum zum Bau des Plans verwendet werden.


In [ ]:
@sy.func2plan(args_shape=[(1, 2), (-1, 2)])
def plan_sum_abs(x, y):
    s = x + y
    return torch.abs(s)

plan_sum_abs.is_built

Auch ist es möglich Zustands-Elemente der Funktion zu übergeben!


In [ ]:
@sy.func2plan(args_shape=[(1,)], state=(torch.tensor([1]), ))
def plan_abs(x, state):
    bias, = state.read()
    x = x.abs()
    return x + bias

In [ ]:
pointer_plan = plan_abs.send(device_1)
x_ptr = torch.tensor([-1, 0]).send(device_1)
p = pointer_plan(x_ptr)
p.get()

Um das Wissen zu vertiefen, kann das Tutorial "Part 8 bis" mit der Verwendung von Plänen und Protokollen verwendet werden!

PySyft auf GitHub einen Stern geben!

Der einfachste Weg, unserer Community zu helfen, besteht darin, die GitHub-Repos mit Sternen auszuzeichnen! Dies hilft, das Bewusstsein für die coolen Tools zu schärfen, die wir bauen.

Nutze unsere Tutorials auf GitHub!

Wir haben hilfreiche Tutorials erstellt, um ein Verständnis für Federated und Privacy-Preserving Learning zu entwickeln und zu zeigen wie wir die einzelnen Bausteine weiter entwickeln.

Mach mit bei Slack!

Der beste Weg, um über die neuesten Entwicklungen auf dem Laufenden zu bleiben, ist, sich unserer Community anzuschließen! Sie können dies tun, indem Sie das Formular unter http://slack.openmined.org ausfüllen.

Treten Sie einem Code-Projekt bei!

Der beste Weg, um zu unserer Community beizutragen, besteht darin, Entwickler zu werden! Sie können jederzeit zur PySyft GitHub Issues-Seite gehen und nach "Projects" filtern. Dies zeigt Ihnen alle Top-Level-Tickets und gibt einen Überblick darüber, an welchen Projekten Sie teilnehmen können! Wenn Sie nicht an einem Projekt teilnehmen möchten, aber ein wenig programmieren möchten, können Sie auch nach weiteren "einmaligen" Miniprojekten suchen, indem Sie nach GitHub-Problemen suchen, die als "good first issue" gekennzeichnet sind.

Spenden

Wenn Sie keine Zeit haben, zu unserer Codebase beizutragen, aber dennoch Unterstützung leisten möchten, können Sie auch Unterstützer unseres Open Collective werden. Alle Spenden fließen in unser Webhosting und andere Community-Ausgaben wie Hackathons und Meetups!


In [ ]: